// <copyright file="Proxy.cs" company="Selenium Committers">
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
// </copyright>

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text.Json.Serialization;

namespace OpenQA.Selenium;

/// <summary>
/// Describes the kind of proxy.
/// </summary>
/// <remarks>
/// Keep these in sync with the Firefox preferences numbers:
/// http://kb.mozillazine.org/Network.proxy.type
/// </remarks>
public enum ProxyKind
{
    /// <summary>
    ///  Direct connection, no proxy (default on Windows).
    /// </summary>
    Direct = 0,

    /// <summary>
    /// Manual proxy settings (e.g., for httpProxy).
    /// </summary>
    Manual,

    /// <summary>
    /// Proxy automatic configuration from URL.
    /// </summary>
    ProxyAutoConfigure,

    /// <summary>
    /// Use proxy automatic detection.
    /// </summary>
    AutoDetect = 4,

    /// <summary>
    /// Use the system values for proxy settings (default on Linux).
    /// </summary>
    System,

    /// <summary>
    /// No proxy type is specified.
    /// </summary>
    Unspecified
}

/// <summary>
/// Describes proxy settings to be used with a driver instance.
/// </summary>
public class Proxy
{
    private ProxyKind proxyKind = ProxyKind.Unspecified;
    private bool isAutoDetect;
    private string? httpProxyLocation;
    private string? proxyAutoConfigUrl;
    private string? sslProxyLocation;
    private string? socksProxyLocation;
    private string? socksUserName;
    private string? socksPassword;
    private int? socksVersion;
    private List<string> noProxyAddresses = new List<string>();

    /// <summary>
    /// Initializes a new instance of the <see cref="Proxy"/> class.
    /// </summary>
    public Proxy()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="Proxy"/> class with the given proxy settings.
    /// </summary>
    /// <param name="settings">A dictionary of settings to use with the proxy.</param>
    /// <exception cref="ArgumentNullException">If <paramref name="settings"/> is <see langword="null"/>.</exception>
    /// <exception cref="ArgumentException">If The "noProxy" value is a list with a <see langword="null"/> element.</exception>
    public Proxy(Dictionary<string, object> settings)
    {
        if (settings == null)
        {
            throw new ArgumentNullException(nameof(settings), "settings dictionary cannot be null");
        }

        if (settings.TryGetValue("proxyType", out object? proxyTypeObj) && proxyTypeObj?.ToString() is string proxyType)
        {
            // Special-case "PAC" since that is the correct serialization.
            if (proxyType.Equals("pac", StringComparison.InvariantCultureIgnoreCase))
            {
                this.Kind = ProxyKind.ProxyAutoConfigure;
            }
            else
            {
                ProxyKind rawType = (ProxyKind)Enum.Parse(typeof(ProxyKind), proxyType, ignoreCase: true);
                this.Kind = rawType;
            }
        }

        if (settings.TryGetValue("httpProxy", out object? httpProxyObj) && httpProxyObj?.ToString() is string httpProxy)
        {
            this.HttpProxy = httpProxy;
        }

        if (settings.TryGetValue("noProxy", out object? noProxy) && noProxy != null)
        {
            List<string> bypassAddresses = new List<string>();
            if (noProxy is string addressesAsString)
            {
                bypassAddresses.AddRange(addressesAsString.Split(';'));
            }
            else
            {
                if (noProxy is object?[] addressesAsArray)
                {
                    foreach (object? address in addressesAsArray)
                    {
                        bypassAddresses.Add(address?.ToString() ?? throw new ArgumentException("Proxy bypass address list \"noProxy\" contained a null element", nameof(settings)));
                    }
                }
            }

            this.AddBypassAddresses(bypassAddresses);
        }

        if (settings.TryGetValue("proxyAutoconfigUrl", out object? proxyAutoconfigUrlObj) && proxyAutoconfigUrlObj?.ToString() is string proxyAutoconfigUrl)
        {
            this.ProxyAutoConfigUrl = proxyAutoconfigUrl;
        }

        if (settings.TryGetValue("sslProxy", out object? sslProxyObj) && sslProxyObj?.ToString() is string sslProxy)
        {
            this.SslProxy = sslProxy;
        }

        if (settings.TryGetValue("socksProxy", out object? socksProxyObj) && socksProxyObj?.ToString() is string socksProxy)
        {
            this.SocksProxy = socksProxy;
        }

        if (settings.TryGetValue("socksUsername", out object? socksUsernameObj) && socksUsernameObj?.ToString() is string socksUsername)
        {
            this.SocksUserName = socksUsername;
        }

        if (settings.TryGetValue("socksPassword", out object? socksPasswordObj) && socksPasswordObj?.ToString() is string socksPassword)
        {
            this.SocksPassword = socksPassword;
        }

        if (settings.TryGetValue("socksVersion", out object? socksVersion) && socksVersion != null)
        {
            this.SocksVersion = Convert.ToInt32(socksVersion);
        }

        if (settings.TryGetValue("autodetect", out object? autodetect) && autodetect != null)
        {
            this.IsAutoDetect = Convert.ToBoolean(autodetect);
        }
    }

    /// <summary>
    /// Gets or sets the type of proxy.
    /// </summary>
    [JsonIgnore]
    public ProxyKind Kind
    {
        get => this.proxyKind;

        set
        {
            this.VerifyProxyTypeCompatilibily(value);
            this.proxyKind = value;
        }
    }

    /// <summary>
    /// Gets the type of proxy as a string for JSON serialization.
    /// </summary>
    [JsonPropertyName("proxyType")]
    public string SerializableProxyKind
    {
        get
        {
            if (this.proxyKind == ProxyKind.ProxyAutoConfigure)
            {
                return "pac";
            }

            return this.proxyKind.ToString().ToLowerInvariant();
        }
    }

    /// <summary>
    /// Gets or sets a value indicating whether the proxy uses automatic detection.
    /// </summary>
    [JsonIgnore]
    public bool IsAutoDetect
    {
        get => this.isAutoDetect;

        set
        {
            if (this.isAutoDetect == value)
            {
                return;
            }

            this.VerifyProxyTypeCompatilibily(ProxyKind.AutoDetect);
            this.proxyKind = ProxyKind.AutoDetect;
            this.isAutoDetect = value;
        }
    }

    /// <summary>
    /// Gets or sets the value of the proxy for the HTTP protocol.
    /// </summary>
    [JsonPropertyName("httpProxy")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string? HttpProxy
    {
        get => this.httpProxyLocation;

        set
        {
            this.VerifyProxyTypeCompatilibily(ProxyKind.Manual);
            this.proxyKind = ProxyKind.Manual;
            this.httpProxyLocation = value;
        }
    }

    /// <summary>
    /// Gets the list of address for which to bypass the proxy as an array.
    /// </summary>
    [JsonPropertyName("noProxy")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public ReadOnlyCollection<string>? BypassProxyAddresses
    {
        get
        {
            if (this.noProxyAddresses.Count == 0)
            {
                return null;
            }

            return this.noProxyAddresses.AsReadOnly();
        }
    }

    /// <summary>
    /// Gets or sets the URL used for proxy automatic configuration.
    /// </summary>
    [JsonPropertyName("proxyAutoconfigUrl")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string? ProxyAutoConfigUrl
    {
        get => this.proxyAutoConfigUrl;

        set
        {
            this.VerifyProxyTypeCompatilibily(ProxyKind.ProxyAutoConfigure);
            this.proxyKind = ProxyKind.ProxyAutoConfigure;
            this.proxyAutoConfigUrl = value;
        }
    }

    /// <summary>
    /// Gets or sets the value of the proxy for the SSL protocol.
    /// </summary>
    [JsonPropertyName("sslProxy")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string? SslProxy
    {
        get => this.sslProxyLocation;

        set
        {
            this.VerifyProxyTypeCompatilibily(ProxyKind.Manual);
            this.proxyKind = ProxyKind.Manual;
            this.sslProxyLocation = value;
        }
    }

    /// <summary>
    /// Gets or sets the value of the proxy for the SOCKS protocol.
    /// </summary>
    [JsonPropertyName("socksProxy")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string? SocksProxy
    {
        get => this.socksProxyLocation;

        set
        {
            this.VerifyProxyTypeCompatilibily(ProxyKind.Manual);
            this.proxyKind = ProxyKind.Manual;
            this.socksProxyLocation = value;
        }
    }

    /// <summary>
    /// Gets or sets the value of username for the SOCKS proxy.
    /// </summary>
    [JsonPropertyName("socksUsername")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string? SocksUserName
    {
        get => this.socksUserName;

        set
        {
            this.VerifyProxyTypeCompatilibily(ProxyKind.Manual);
            this.proxyKind = ProxyKind.Manual;
            this.socksUserName = value;
        }
    }

    /// <summary>
    /// Gets or sets the value of the protocol version for the SOCKS proxy.
    /// Value can be <see langword="null"/> if not set.
    /// </summary>
    [JsonPropertyName("socksVersion")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public int? SocksVersion
    {
        get => this.socksVersion;

        set
        {
            if (value == null)
            {
                this.socksVersion = value;
            }
            else
            {
                if (value.Value <= 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "SocksVersion must be a positive integer");
                }

                this.VerifyProxyTypeCompatilibily(ProxyKind.Manual);
                this.proxyKind = ProxyKind.Manual;
                this.socksVersion = value;
            }
        }
    }

    /// <summary>
    /// Gets or sets the value of password for the SOCKS proxy.
    /// </summary>
    [JsonPropertyName("socksPassword")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string? SocksPassword
    {
        get => this.socksPassword;

        set
        {
            this.VerifyProxyTypeCompatilibily(ProxyKind.Manual);
            this.proxyKind = ProxyKind.Manual;
            this.socksPassword = value;
        }
    }

    /// <summary>
    /// Adds a single address to the list of addresses against which the proxy will not be used.
    /// </summary>
    /// <param name="address">The address to add.</param>
    public void AddBypassAddress(string address)
    {
        if (string.IsNullOrEmpty(address))
        {
            throw new ArgumentException("address must not be null or empty", nameof(address));
        }

        this.AddBypassAddresses(address);
    }

    /// <summary>
    /// Adds addresses to the list of addresses against which the proxy will not be used.
    /// </summary>
    /// <param name="addressesToAdd">An array of addresses to add.</param>
    public void AddBypassAddresses(params string[] addressesToAdd)
    {
        this.AddBypassAddresses((IEnumerable<string>)addressesToAdd);
    }

    /// <summary>
    /// Adds addresses to the list of addresses against which the proxy will not be used.
    /// </summary>
    /// <param name="addressesToAdd">An <see cref="IEnumerable{T}"/> object of arguments to add.</param>
    public void AddBypassAddresses(IEnumerable<string> addressesToAdd)
    {
        if (addressesToAdd == null)
        {
            throw new ArgumentNullException(nameof(addressesToAdd), "addressesToAdd must not be null");
        }

        this.VerifyProxyTypeCompatilibily(ProxyKind.Manual);
        this.proxyKind = ProxyKind.Manual;
        this.noProxyAddresses.AddRange(addressesToAdd);
    }

    /// <summary>
    /// Returns a dictionary suitable for serializing to the W3C Specification
    /// dialect of the wire protocol.
    /// </summary>
    /// <returns>A dictionary suitable for serializing to the W3C Specification
    /// dialect of the wire protocol.</returns>
    internal Dictionary<string, object?>? ToCapability()
    {
        return this.AsDictionary(true);
    }

    /// <summary>
    /// Returns a dictionary suitable for serializing to the OSS dialect of the
    /// wire protocol.
    /// </summary>
    /// <returns>A dictionary suitable for serializing to the OSS dialect of the
    /// wire protocol.</returns>
    internal Dictionary<string, object?>? ToLegacyCapability()
    {
        return this.AsDictionary(false);
    }

    private Dictionary<string, object?>? AsDictionary(bool isSpecCompliant)
    {
        Dictionary<string, object?>? serializedDictionary = null;
        if (this.proxyKind != ProxyKind.Unspecified)
        {
            serializedDictionary = new Dictionary<string, object?>();
            if (this.proxyKind == ProxyKind.ProxyAutoConfigure)
            {
                serializedDictionary["proxyType"] = "pac";
                if (!string.IsNullOrEmpty(this.proxyAutoConfigUrl))
                {
                    serializedDictionary["proxyAutoconfigUrl"] = this.proxyAutoConfigUrl;
                }
            }
            else
            {
                serializedDictionary["proxyType"] = this.proxyKind.ToString().ToLowerInvariant();
            }

            if (!string.IsNullOrEmpty(this.httpProxyLocation))
            {
                serializedDictionary["httpProxy"] = this.httpProxyLocation;
            }

            if (!string.IsNullOrEmpty(this.sslProxyLocation))
            {
                serializedDictionary["sslProxy"] = this.sslProxyLocation;
            }

            if (!string.IsNullOrEmpty(this.socksProxyLocation))
            {
                if (!this.socksVersion.HasValue)
                {
                    throw new InvalidOperationException("Must have a version value set (usually 4 or 5) when specifying a SOCKS proxy");
                }

                string socksAuth = string.Empty;
                if (!string.IsNullOrEmpty(this.socksUserName) && !string.IsNullOrEmpty(this.socksPassword))
                {
                    // TODO: this is probably inaccurate as to how this is supposed
                    // to look.
                    socksAuth = this.socksUserName + ":" + this.socksPassword + "@";
                }

                serializedDictionary["socksProxy"] = socksAuth + this.socksProxyLocation;
                serializedDictionary["socksVersion"] = this.socksVersion.Value;
            }

            if (this.noProxyAddresses.Count > 0)
            {
                serializedDictionary["noProxy"] = this.GetNoProxyAddressList(isSpecCompliant);
            }
        }

        return serializedDictionary;
    }

    private object? GetNoProxyAddressList(bool isSpecCompliant)
    {
        object? addresses = null;
        if (isSpecCompliant)
        {
            List<object> addressList = [.. this.noProxyAddresses];
            addresses = addressList;
        }
        else
        {
            addresses = this.BypassProxyAddresses;
        }

        return addresses;
    }

    private void VerifyProxyTypeCompatilibily(ProxyKind compatibleProxy)
    {
        if (this.proxyKind != ProxyKind.Unspecified && this.proxyKind != compatibleProxy)
        {
            string errorMessage = string.Format(
                CultureInfo.InvariantCulture,
                "Specified proxy type {0} is not compatible with current setting {1}",
                compatibleProxy.ToString().ToUpperInvariant(),
                this.proxyKind.ToString().ToUpperInvariant());

            throw new InvalidOperationException(errorMessage);
        }
    }
}
